import math
import struct
from enum import Enum

class ColorType(Enum):
    BGR   = 1
    YIQ   = 2
    HSI   = 3
    YCbCr = 4
    XYZ   = 5

class ConvertType(Enum):
    BGR2YIQ   = 1
    BGR2HSI   = 2
    BGR2YCbCr = 3
    BGR2XYZ   = 4

class Scalar:

    '''
    Initialization of Scalar.
    '''
    def __init__(self, Param1, Param2, Param3):
        self.channel1 = Param1
        self.channel2 = Param2
        self.channel3 = Param3

    '''
    Use string to present the information of Scalar.
    '''
    def toString(self):
        return '(' + str(self.channel1) + ', ' + str(self.channel2) + ', ' + str(self.channel3) + ')'

class Mat:

    '''
    Initialization of Mat.
    '''
    def __init__(self, cols, rows, Code=1, HeadInformation=0):
        self.cols = cols
        self.rows = rows
        self.info = [[0 for i in range(0, cols)] for j in range(0, rows)]
        self.ColorType = Code
        self.headInformation = HeadInformation

    '''
    Get point information of given location.
    '''
    def at(self, col, row):
        return self.info[row][col]

    '''
    Change point information of given location.
    '''
    def change(self, col, row, input):
        self.info[row][col] = input

class cv:

    '''
    Return the bigger number.
    '''
    def max(self, Input1, Input2):
        if Input1 > Input2:
            return Input1
        else:
            return Input2

    '''
    Return the smaller number.
    '''
    def min(self, Input1, Input2):
        if Input1 < Input2:
            return Input1
        else:
            return Input2

    '''
    Representationmedium of the data that is not between 0 and 255
    '''
    def Representationmedium(self, Input):
        result = int(Input)
        if result > 255:
            result = 255
        elif result < 0:
            result = 0
        else:
            result = int(result)
        return result

    '''
    A simple way to read .bmp picture. 
    The method is able to read the .bmp picture with the file head that is over 54 bytes.
    '''
    def imread(self, FilePath):
        fileReader = open(FilePath, 'rb')
        # Read head bytes.
        bmpHead = fileReader.read(54)
        headInformation = struct.unpack('<HIIIIIIHHIIIIII', bmpHead)
        # Some important information.
        offsetSize = headInformation[3]
        cols = headInformation[5]
        rows = headInformation[6]
        pointByteSize = headInformation[8] >> 3
        imageSize = headInformation[10]
        # Calculate the size of fill bytes.
        fillSize = (imageSize - (pointByteSize * cols * rows)) / rows
        # Find the begin of picture data.
        if offsetSize != 54:
            fileReader.read(offsetSize - 54)
        result = Mat(cols, rows, ColorType.BGR, bmpHead)
        for row in range(0, rows):
            for col in range(0, cols):
                # Read data of point.
                point = fileReader.read(pointByteSize)
                colors = struct.unpack('<BBB', point)
                colorOfPoint = Scalar(colors[2], colors[1], colors[0])
                result.change(col, rows - row - 1, colorOfPoint)
            # Read fill bytes.
            fileReader.read(fillSize)
        return result

    '''
    Provide a simple way to save .bmp pictures.
    '''
    def imwrite(self, FilePath, InputArray):
        # Initial the head bytes.
        bmpHead = InputArray.headInformation
        headInformation = struct.unpack('<HIIIIIIHHIIIIII', bmpHead)
        cols = headInformation[5]
        rows = headInformation[6]
        pointByteSize = headInformation[8] >> 3
        imageSize = headInformation[10]
        # Calculate the size of fill bytes and initialization.
        fillSize = (imageSize - (pointByteSize * cols * rows)) / rows
        if fillSize != 0:
            fillByte = struct.pack('<B', 0)
            if fillSize > 1:
                for i in range(0, fillSize-1):
                    fillByte += struct.pack('<B', 0)
        fileWriter = open(FilePath, 'wb')
        fileWriter.write(bmpHead)
        for row in range(0, InputArray.rows):
            for col in range(0, InputArray.cols):
                # Write data of point.
                Channel1 = struct.pack('<B', InputArray.at(col, InputArray.rows - row - 1).channel1)
                Channel2 = struct.pack('<B', InputArray.at(col, InputArray.rows - row - 1).channel2)
                Channel3 = struct.pack('<B', InputArray.at(col, InputArray.rows - row - 1).channel3)
                fileWriter.write(Channel1)
                fileWriter.write(Channel2)
                fileWriter.write(Channel3)
            # Write fill bytes.
            fileWriter.write(fillByte)

    '''
    Converting between several types of color space.
    The core method of the experiment.
    '''
    def cvtColor(self, InputArray, OutputArray, Code):
        # Initial head information for file writing later.
        OutputArray.headInformation = InputArray.headInformation
        for col in range(0, InputArray.cols):
            for row in range(0, InputArray.rows):
                # Initial RGB vector.
                R = InputArray.at(col, row).channel1
                G = InputArray.at(col, row).channel2
                B = InputArray.at(col, row).channel3
                # Convert picture from RGB to other color space.
                if Code == ConvertType.BGR2XYZ:
                    Channel1 =   0.490 * R +  0.310 * G +  0.200 * B
                    Channel2 =   0.177 * R +  0.813 * G +  0.011 * B
                    Channel3 =   0.000 * R +  0.010 * G +  0.990 * B
                elif Code == ConvertType.BGR2YCbCr:
                    Channel1 =   0.299 * R +  0.587 * G +  0.114 * B
                    Channel2 = -0.1687 * R - 0.3313 * G +    0.5 * B + 128
                    Channel3 =     0.5 * R - 0.4187 * G - 0.0813 * B + 128
                elif Code == ConvertType.BGR2YIQ:
                    Channel1 =   0.299 * R +  0.587 * G +  0.114 * B
                    Channel2 =   0.596 * R -  0.274 * G -  0.322 * B
                    Channel3 =   0.211 * R -  0.523 * G +  0.312 * B
                    Channel2 = Channel2 + 128
                    Channel3 = Channel3 + 128
                elif Code == ConvertType.BGR2HSI:
                    R = float(R)
                    G = float(G)
                    B = float(B)
                    if R + G + B == 0:
                        r = 0
                        g = 0
                        b = 0
                    else:
                        r = R / (R + G + B)
                        g = G / (R + G + B)
                        b = B / (R + G + B)
                    den = math.sqrt(pow(r - g, 2) + (r - b) * (g - b))
                    if b <= g:
                        if den == 0:
                            h = 0
                        else:
                            temp = (r - g + r - b) / (2 * den)
                            if temp > 1:
                                temp = 1
                            elif temp < -1:
                                temp = -1
                            else:
                                temp = temp
                            h = math.acos(temp)
                    else:
                        if den == 0:
                            h = 2 * math.pi
                        else:
                            h = 2 * math.pi - math.acos(temp)
                    s = 1 - 3 * self.min(self.min(r, g), b)
                    i = (R + G + B) / 3
                    Channel1 = h / (2 * math.pi) * 255
                    Channel2 = s * 255
                    Channel3 = i
                # Representationmedium of data.
                Channel1 = self.Representationmedium(Channel1)
                Channel2 = self.Representationmedium(Channel2)
                Channel3 = self.Representationmedium(Channel3)
                # Get the result.
                newPoint = Scalar(Channel1, Channel2, Channel3)
                OutputArray.change(col, row, newPoint)
